﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using HWorld;
using HWorld.Networking;
using NUnrar;
using OClick;
using ROrganizer.Client;
using ROrganizer.Client.Files;
using RSyndication;
using RSyndication.Google;

namespace ROrganizer.Client.Crawling
{
    public class Crawler
    {
        private bool isWorking;
        private Thread thread;

        private Logger logger;
        private HNetworkSession session;
        private RChannel channel;
        private OFacade oclick;

        public Crawler(OFacade oclick, HNetworkSession session, Logger logger = null)
        {
            this.logger = logger ?? new Logger();
            this.oclick = oclick;
            this.session = session;
            this.channel = new RGoogleChannel(RNetwork.Create(session));
        }

        public event EventHandler Changed;
        private void OnChanged()
        {
            if (this.Changed != null)
                this.Changed(this, EventArgs.Empty);
        }

        public bool IsWorking
        {
            get { return this.isWorking; }
        }

        public void Start(string url, int start = 0, int count = 20)
        {
            lock (this.channel)
            {
                this.isWorking = true;
                this.OnChanged();
            }

            this.logger.OnLog(Guid.Empty, "Starting crawling '{0}'...", url);
            ThreadPool.QueueUserWorkItem(this.OnCallback, new ThreadStartup { Url = url, Start = start, Count = count });
        }

        public void Stop()
        {
            if (this.thread != null)
                this.thread.Abort();
        }

        private void OnCallback(object state)
        {
            lock (this.channel)
            {
                this.isWorking = true;
                this.thread = Thread.CurrentThread;
                this.OnChanged();
            }

            try
            {
                while (true)
                {
                    try
                    {
                        var startup = state as ThreadStartup;
                        var messages = this.channel.GetMessages(startup.Url, offset: startup.Start, count: startup.Count);
                        var repository = new MessageRepository();
                        var position = 0;

                        this.logger.OnLog(Guid.Empty, "Found '{0}' messages.", messages.Length);

                        foreach (var message in messages)
                        {
                            var text = message.Description;
                            var document = HDocumentFactory.Create(text);

                            this.logger.OnLog(message.Id, "Message: {2}, {0} ({1:dd MMMM yyyy})", message.Title, message.Published, position++);

                        start:

                            var imdbs = from element in HQuery.On(document).Execute("a[href]")
                                        where element.Attributes[HAttributeType.Hyperlink].StartsWith("http://imdb.com/", StringComparison.InvariantCultureIgnoreCase)
                                            || element.Attributes[HAttributeType.Hyperlink].StartsWith("http://www.imdb.com/", StringComparison.InvariantCultureIgnoreCase)
                                        select element.Attributes[HAttributeType.Hyperlink];

                            if (imdbs.Distinct().Count() != 1)
                            {
                                if (text == message.Description)
                                {
                                    using (var data = this.session.Get(message.Link))
                                    {
                                        text = data.GetText();
                                        document = HDocumentFactory.Create(text);
                                    }

                                    goto start;
                                }

                                this.logger.OnLog(message.Id, "No valid IMDB link found.");
                                continue;
                            }

                            var dlinks = from element in HQuery.On(document).Execute("a[href]")
                                         let reference = this.oclick.CanProcess(element.Attributes[HAttributeType.Hyperlink])
                                         where reference != null
                                         select new
                                         {
                                             Reference = reference,
                                             Link = element.Attributes[HAttributeType.Hyperlink]
                                         };

                            var ilinks = from element in document.Descendants<HText>()
                                         from word in element.Value.Split(new[] { ' ', '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)
                                         let reference = this.oclick.CanProcess(word)
                                         where reference != null
                                         select new
                                         {
                                             Reference = reference,
                                             Link = word
                                         };

                            var groups = from link in dlinks.Union(ilinks)
                                         group link by link.Reference.Id into grouped
                                         select new
                                         {
                                             Reference = grouped.First().Reference,
                                             Links = grouped.Select(x => new OResource(message.Id, x.Link)).ToArray()
                                         };

                            var queries = from lgroup in groups
                                          select new
                                          {
                                              Reference = lgroup.Reference,
                                              Links = this.oclick.Query(lgroup.Reference.Id, lgroup.Links)
                                          };

                            var orders = new[] { "Megaupload.com", "Rapidshare.com" }.ToList();
                            var infos = from query in queries
                                        where orders.Contains(query.Reference.Name)
                                        from link in query.Links
                                        where link.Name.EndsWith(".part1.rar")
                                            || link.Name.EndsWith(".part01.rar")
                                            || link.Name.EndsWith(".part001.rar")
                                            || link.Name.EndsWith(".avi")
                                        orderby orders.IndexOf(query.Reference.Name)
                                        select new
                                        {
                                            Reference = query.Reference,
                                            Link = link
                                        };

                            var found = infos.FirstOrDefault();
                            if (found == null)
                            {
                                if (text == message.Description)
                                {
                                    using (var data = this.session.Get(message.Link))
                                    {
                                        text = data.GetText();
                                        document = HDocumentFactory.Create(text);
                                    }

                                    goto start;
                                }

                                this.logger.OnLog(message.Id, "No downloadable files found.");
                                continue;
                            }

                            var files = infos.Where(x => x.Reference.Id == found.Reference.Id).ToArray();
                            var pack = default(MoviePack);
                            var imdb = this.GetImdb(imdbs.First());

                            if (String.IsNullOrWhiteSpace(imdb.Id))
                            {
                                this.logger.OnLog(message.Id, "Bad IMDB entry. Skipping...");
                                continue;
                            }

                            this.logger.OnLog(message.Id, "Found movie: {0}", imdb.Title);
                            this.logger.OnLog(message.Id, "Found files: {0}.", files.Select(x => x.Link.Link).Distinct().Count());

                            try
                            {
                                foreach (var file in files)
                                {
                                    var entry = repository.Get(file.Link.Link);
                                    if (entry != null)
                                        continue;

                                    this.logger.OnLog(message.Id, "Processing '{0}'...", file.Link.Link);

                                    if (file.Link.Name.EndsWith(".avi"))
                                        pack = this.DownloadMovie(message, file.Reference.Id, file.Link.Link);
                                    else
                                        pack = this.DownloadArchive(message, file.Reference.Id, file.Link.Link);

                                    using (var stream = new MemoryStream(pack.Data, false))
                                    {
                                        var mFacility = new MetaFacility();
                                        pack.Meta = mFacility.AnalyzeFile(stream);

                                        stream.Seek(0, SeekOrigin.Begin);
                                        pack.Extension = mFacility.GetExtension(stream);

                                        stream.Seek(0, SeekOrigin.Begin);
                                        var hFacility = new HashFacility();
                                        pack.Hash = hFacility.ComputeHash(stream);

                                    }

                                    pack.Imdb = imdb;
                                    pack.Source = file.Link.Link;

                                    if (pack.Imdb == null)
                                    {
                                        this.logger.OnLog(message.Id, "Cannot load IMDB entry.");
                                        continue;
                                    }

                                    if (pack.Meta == null)
                                    {
                                        this.logger.OnLog(message.Id, "Cannot analyze file metadata.");
                                        this.Persist(pack);
                                        continue;
                                    }

                                    if (pack.Data.Length < 10 * 1024 * 1024)
                                    {
                                        this.logger.OnLog(message.Id, "The file does not meet size requirements.");
                                        continue;
                                    }

                                    this.Persist(repository, message, pack);
                                    this.logger.OnLog(message.Id, "Processing '{0}' completed.", file.Link.Link);
                                }

                                Thread.Sleep(TimeSpan.FromSeconds(1));
                            }
                            catch (OException ex)
                            {
                                this.logger.OnLog(message.Id, ex.Message);

                                if (!ex.Local)
                                {
                                    if (ex.Postpone != default(TimeSpan))
                                        Thread.Sleep(ex.Postpone);
                                    else if (ex.Simultaneity)
                                        Thread.Sleep(TimeSpan.FromMinutes(5));
                                }
                            }
                            catch (InvalidRarFormatException ex)
                            {
                                this.logger.OnLog(message.Id, ex.Message);
                            }
                            catch (IOException ex)
                            {
                                this.logger.OnLog(message.Id, ex.Message);
                                this.logger.OnLog(message.Id, "Waiting 1 minute...");
                                Thread.Sleep(TimeSpan.FromMinutes(1));
                            }
                        }

                        this.logger.OnLog(Guid.Empty, "Waiting 15 minutes...");
                        Thread.Sleep(TimeSpan.FromMinutes(15));
                    }
                    catch (ThreadAbortException)
                    {
                        this.logger.OnLog(Guid.Empty, "Crawler stopped.");
                    }
                    catch (Exception ex)
                    {
                        this.logger.OnLog(Guid.Empty, ex.Message);
                        this.logger.OnLog(Guid.Empty, "Waiting 5 minutes...");
                        Thread.Sleep(TimeSpan.FromMinutes(5));
                    }
                }
            }
            finally
            {
                lock (this.channel)
                {
                    this.isWorking = false;
                    this.thread = null;
                    this.OnChanged();
                }
            }
        }

        private void Persist(MessageRepository repository, RMessage message, MoviePack pack)
        {
            var hash = pack.Hash;
            var name = FileProcessor.EspaceTitle(pack.Imdb.Title);
            var path = Path.Combine(@"M:\Crawler", name + " - " + hash + pack.Extension);

            File.WriteAllBytes(path, pack.Data);

            var entry = new MessageEntry
            {
                Id = message.Id,
                Title = message.Title,
                Link = message.Link,
                Source = pack.Source,
                Size = pack.Size,
                Imdb = pack.Imdb.Id,
                Hash = pack.Hash,
                Movie = pack.Imdb.Title,
                Location = path,
            };

            repository.SaveOrUpdate(entry);
        }

        private void Persist(MoviePack pack)
        {
            var hash = pack.Hash;
            var name = FileProcessor.EspaceTitle(pack.Imdb.Title);
            var path = Path.Combine(@"M:\Failures", name + " - " + hash + pack.Extension);

            File.WriteAllBytes(path, pack.Data);
        }

        private ImdbInfo GetImdb(string link)
        {
            var iFacility = new ImdbFacility();
            var iGateway = new ImdbGateway();

            var id = iFacility.GetId(link);
            var info = iGateway.Query(id);

            return info;
        }

        private MoviePack DownloadMovie(RMessage message, Guid provider, string link)
        {
            using (var download = this.oclick.Download(provider, new OResource(message.Id, link)))
            using (var stream = download.OpenStream())
            {
                var data = new byte[10 * 1024 * 1024];
                var left = data.Length;

                this.logger.OnLog(Guid.Empty, "Retrieving '{0}' ({1} MB)...", download.Name, download.Size / 1024 / 1024);

                if (download.Size <= 600 * 1024 * 1024)
                    return new MoviePack
                    {
                        Data = new byte[0],
                        Size = download.Size,
                        Name = download.Name
                    };

                while (left > 0)
                {
                    var read = stream.Read(data, data.Length - left, left);
                    left -= read;
                }

                return new MoviePack { Data = data, Size = download.Size, Name = download.Name };
            }
        }

        private MoviePack DownloadArchive(RMessage message, Guid provider, string link)
        {
            using (var download = this.oclick.Download(provider, new OResource(message.Id, link)))
            using (var stream = download.OpenStream())
            using (var destination = new LimitedStream(10 * 1024 * 1024))
            {
//                var reader = RarReader.Open(File.OpenRead(@"M:\tkufuki8ki.part01.rar"));
                var reader = RarReader.Open(stream);
                var streams = new List<Stream>();
                var size = 0L;
                var name = String.Empty;
                var loaded = 0L;

                try
                {
                    reader.WritableStreamRequired += info =>
                    {
                        if (info.Size > 600 * 1024 * 1024)
                        {
                            size = info.Size;
                            name = info.FilePath;

                            this.logger.OnLog(Guid.Empty, "Retrieving '{0}' ({1} MB)...", info.FilePath, info.Size / 1024 / 1024);
                            return destination;
                        }

                        if (Regex.IsMatch(info.FilePath, "r(ar|[0-9]{2})$", RegexOptions.IgnoreCase) && info.Size > 10 * 1024 * 1024)
                            throw new LimitedStream.Exception();

                        this.logger.OnLog(Guid.Empty, "Skipping '{0}' ({1} kB)...", info.FilePath, info.Size / 1024);

                        if (loaded + info.Size > 120 * 1024 * 1024)
                            throw new LimitedStream.Exception();

                        var mstream = new MemoryStream();
                        streams.Add(mstream);
                        loaded += info.Size;

                        return mstream;
                    };

                    reader.DecompressStream();
                }
                catch (LimitedStream.Exception) { }

                streams.ForEach(mstream => mstream.Dispose());
                return new MoviePack { Data = destination.ToArray(), Size = size, Name = name };
            }
        }

        private class ThreadStartup
        {
            public string Url;
            public int Start;
            public int Count;
        }

        private class MoviePack
        {
            public byte[] Data { get; set; }
            public long Size { get; set; }
            public string Name { get; set; }
            public string Source { get; set; }

            public ImdbInfo Imdb { get; set; }
            public MetaInfo Meta { get; set; }
            public string Hash { get; set; }
            public string Extension { get; set; }
        }
    }
}
